/**
 * 
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 *   
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 */
package org.jiopi.ibean.kernel.context.classloader;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Set;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.Modifier;
import javassist.NotFoundException;

import org.apache.log4j.Logger;
import org.dom4j.Document;
import org.jiopi.framework.core.JiopiConfigConstants;
import org.jiopi.ibean.kernel.IBeanCentralConsoleKernel;
import org.jiopi.ibean.kernel.config.JIOPiConfig;
import org.jiopi.ibean.kernel.context.ContextCentralConsoleKernel;
import org.jiopi.ibean.kernel.context.ControlPanelImpl;
import org.jiopi.ibean.kernel.context.ModuleConsoleImpl;
import org.jiopi.ibean.kernel.context.classloader.RedefineableClassLoader.RedefineClass;
import org.jiopi.ibean.kernel.util.XMLMerger;
import org.jiopi.ibean.kernel.util.ObjectAccessor;
import static org.jiopi.ibean.kernel.context.classloader.RedefineableClassLoader.CONTROL_PANEL_CLASS_FIELD;
import static org.jiopi.ibean.kernel.context.classloader.RedefineableClassLoader.CONTROL_PANEL_FIELD;
import static org.jiopi.ibean.kernel.context.classloader.RedefineableClassLoader.getDeclaredFieldSet;
import static org.jiopi.ibean.kernel.context.classloader.RedefineableClassLoader.getDeclaredMethodSet;

/**
 * 
 * redefine class which loads from context
 * 
 * @version 0.4 2010.6.11
 * @since iBeanKernel0.4 2010.6.11
 *
 */
public class ContextRedefineableClassLoader implements ClassFileTransformer {
	
	private static Logger logger = Logger.getLogger(ContextRedefineableClassLoader.class);
	
	protected final static String INIT_OBJ = "__JIOPI_INIT_OBJ__";
	protected final static String INIT_METHOD = "__JIOPI_INIT_METHOD__";
	
	private final ClassPool pool;
	
	protected final ContextCentralConsoleKernel ccck;
	
	protected final ClassLoader parentClassLoader;
	
	private final HashMap<String,RedefineClass> redefineInfos;
	
	private final HashMap<String,byte[]> redefineClasses = new HashMap<String,byte[]>();
	
	private ContextRedefineableClassLoader(ClassLoader cl,ContextCentralConsoleKernel ccck,HashMap<String,RedefineClass> rc){
		pool = new ClassPool();
		pool.appendClassPath( new LoaderClassPath( cl ) );
		parentClassLoader = cl;
		redefineInfos = rc;
		this.ccck = ccck;
		for(String key:redefineInfos.keySet()){
			redefineClasses.put(key, null);
		}
	}
	
	public static ContextRedefineableClassLoader getContextRedefineableClassLoader(ClassLoader contextClassLoader,ContextCentralConsoleKernel ccck){
		HashMap<String,RedefineClass> redefineClasses = RedefineableClassLoader.getRedefineClasses(contextClassLoader.getResource(JiopiConfigConstants.ASSEMBLING_FILE));
		if(redefineClasses!=null) return new ContextRedefineableClassLoader(contextClassLoader,ccck,redefineClasses);
		return null;
	}
	
	public synchronized void redefineContext(){
		try {
			Class<?>[] classes = new Class<?>[redefineInfos.size()];
			int i=0;
			String[] classNames = redefineInfos.keySet().toArray(new String[redefineInfos.size()]);
			for(String className:classNames){
				classes[i++] = parentClassLoader.loadClass(className);
			}
			Instrumentation ins = IBeanCentralConsoleKernel.getInstrumentation();
			Method method = null;
			try{
				method = Instrumentation.class.getMethod("retransformClasses", new Class<?>[]{Class[].class});
			}catch(Exception e){
				logger.warn("retransformClasses not support , Redefine class may not perfect work");
			}
			if(method!=null){
				method.invoke(ins, new Object[]{classes});
			}
		} catch (Exception e) {
			logger.error("Redefine Classes in Context ERROR!",e);
		}
	}

	public byte[] transform(ClassLoader loader, String className,
			Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
			byte[] classfileBuffer) throws IllegalClassFormatException {
		if(loader == parentClassLoader){
			className = className.replaceAll("/", "."); 
			RedefineClass rc = redefineInfos.get(className);
			if(rc!=null){
				byte[] bytes = redefineClasses.get(className);
				if(bytes == null){
					//System.out.println("transform "+className+" "+classBeingRedefined);
					try{
						
						CtClass ctClass = pool.getCtClass(className);
						ModuleConsoleImpl toModuleConsole = ccck.accessModuleConsole(rc.moduleName, rc.version);
						//System.out.println("controlpanel:"+rc.controlpanel);
						ControlPanelImpl cpi =toModuleConsole.accessStaticControlPanel(rc.registerName);
						Class<?> toClass = (Class<?>)cpi.wrapped;
						
						Class<?> toClassAPI = toModuleConsole.getClassLoader().loadBlueprintClass(rc.controlpanel);
						
						if(classBeingRedefined == null){//not redefine,try add fields
							
							CtField field = null;
							try {
								field = ctClass.getField(CONTROL_PANEL_FIELD);
							} catch (NotFoundException ignored) {
							}
							if(field == null){
								field = CtField.make( "private "+toClassAPI.getName()+" "+CONTROL_PANEL_FIELD+";", ctClass );
								ctClass.addField( field );
							}
							
							CtField fieldc = null;
							try {
								fieldc = ctClass.getField(CONTROL_PANEL_CLASS_FIELD);
							} catch (NotFoundException ignored) {
							}
							if(fieldc == null){
								fieldc = CtField.make( "private static Class "+CONTROL_PANEL_CLASS_FIELD+";", ctClass );
								ctClass.addField( fieldc );
							}
						}
						
						//check if has Instantiation annotation
						boolean needInstantiation = (toModuleConsole.moduleClassLoader.getInstantiationConfig(toClass.getName())!=null);
						//check if has control panel configuration
						if(!needInstantiation){
							URL assemblingConfigFile = parentClassLoader.getResource(JiopiConfigConstants.ASSEMBLING_FILE);
							if(assemblingConfigFile!=null){
								Document assemblingConfigDocument = XMLMerger.readDocumentAndCloseStream(assemblingConfigFile.openStream());
								if(assemblingConfigDocument != null){
									JIOPiConfig assemblingConfig = new JIOPiConfig(assemblingConfigDocument);
									JIOPiConfig.Module findModuleConfig = assemblingConfig.getModuleConfig(rc.moduleName, rc.version);
                                    if(null != findModuleConfig) needInstantiation = (findModuleConfig.getMatchedControlpanel(toModuleConsole.getClassLoader(),rc.controlpanel,rc.configuration)!=null);
								}
							}
						}
						
						//change Constructors
						CtConstructor[] ctConstructors = ctClass.getDeclaredConstructors();
						for( CtConstructor ctcons:ctConstructors ) {
							int modifiers = ctcons.getModifiers();
							if(Modifier.isPublic(modifiers)){
								if(needInstantiation){
									//get real obj at runtime
									//System.out.println("$0."+CONTROL_PANEL_FIELD+"=("+toClassAPI.getName()+")org.jiopi.framework.CentralConsole.accessControlPanel( \""+rc.moduleName+"\",\""+rc.version+"\" , \""+rc.controlpanel+"\", "+toClassAPI.getName()+".class, $args );");
									
									//right one
									ctcons.setBody("$0."+CONTROL_PANEL_FIELD+"=("+toClassAPI.getName()+")org.jiopi.framework.CentralConsole.accessControlPanel( \""+rc.moduleName+"\",\""+rc.version+"\" , \""+rc.registerName+"\", "+toClassAPI.getName()+".class, $args );");
									
									//ctcons.setBody("{System.out.println("+toClassAPI.getName()+".class.getClassLoader());System.out.println(org.jiopi.framework.CentralConsole.accessControlPanel( \""+rc.moduleName+"\",\""+rc.version+"\" , \""+rc.controlpanel+"\", "+toClassAPI.getName()+".class, $args ).getClass().getSuperclass().getClassLoader());}");
								}else{
									//System.out.println(\"begin\"+__JIOPI_CONTROL_PANEL_CLASS_FIELD__.getConstructors());
									//ctcons.setBody("{ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();try{Thread.currentThread().setContextClassLoader("+CONTROL_PANEL_CLASS_FIELD+".getClassLoader());System.out.println(\"begin\"+__JIOPI_CONTROL_PANEL_CLASS_FIELD__.getClassLoader());$0."+CONTROL_PANEL_FIELD+"=("+toClassAPI.getName()+")"+CONTROL_PANEL_CLASS_FIELD+".getConstructor($sig).newInstance($args);}catch(Exception e){throw new RuntimeException(e);}finally{Thread.currentThread().setContextClassLoader( contextClassLoader );}}");
									ctcons.setBody("$0."+CONTROL_PANEL_FIELD+"=("+toClassAPI.getName()+")org.jiopi.framework.CentralConsole.accessControlPanel( \""+rc.moduleName+"\",\""+rc.version+"\" , \""+rc.registerName+"\", "+toClassAPI.getName()+".class, $args );");
								}
							}
						}
						
						{//change methods
							Set<CtMethod>  declaredSet = getDeclaredMethodSet( ctClass );
							CtMethod[] methods = ctClass.getMethods();
							boolean modify;
                            for( CtMethod method:methods ) {
								modify = false;
								int modifiers = method.getModifiers();
								if(Modifier.isPublic(modifiers)) {//only redefine public		
									if( declaredSet.contains( method )) {
										if(RedefineableClassLoader.haveMethod(toClassAPI, method))modify = true;
									}else if(classBeingRedefined==null && !Modifier.isNative( modifiers ) 
												&& !Modifier.isFinal( modifiers )
												&& !Modifier.isAbstract( modifiers ) ) {
											method = CtNewMethod.copy( method, ctClass, null );		// added
											ctClass.addMethod( method );
											modify = true;
									}
								}
								if( modify ) {
									String methodName = method.getName();
									if(Modifier.isStatic(modifiers)){
										method.setBody("{return ($r)"+CONTROL_PANEL_CLASS_FIELD+".getMethod(\""+methodName+"\",$sig).invoke(null,$args);}");
										//method.setBody("{System.out.print(\"aaa\");return null;}");
									}else{
										method.setBody( "{return "+"$0."+CONTROL_PANEL_FIELD+"."+methodName+"($$);}" );
									}
								}
							}
						}
						
						//Change public static fields
						ArrayList<CtField> changeStaticList = new ArrayList<CtField>();
						{
							Set<CtField>  declaredSet = getDeclaredFieldSet( ctClass );
							CtField[] fields = ctClass.getFields();
							for(CtField ctField:fields){
								int modifiers = ctField.getModifiers();
								if(Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers) && RedefineableClassLoader.haveField(toClassAPI,ctField)){
									if(classBeingRedefined == null &&!declaredSet.contains(ctField)){
										CtField newCtField = null;
										try{
											newCtField = ctClass.getField(ctField.getName());
										}catch(NotFoundException ignored){
										}
										if(newCtField == null){
											newCtField = new CtField(ctField,ctClass);
											ctClass.addField(newCtField);
										}
									}
									changeStaticList.add(ctField);
								}
							}
						}
						
						CtMethod initMethod;
						if(classBeingRedefined == null){//try add
							initMethod = CtNewMethod.make("private static Object "+INIT_METHOD+"(){return null;}", ctClass);
							ctClass.addMethod(initMethod);
						}
						
						
						//init static fields
						
							if(classBeingRedefined == null){
								initMethod = ctClass.getDeclaredMethod(INIT_METHOD);
								if(initMethod!=null){
									
									StringBuilder initMethodContent = new StringBuilder("{");
									//init CONTROL_PANEL_CLASS_FIELD
									initMethodContent.append("org.jiopi.framework.ControlPanel cpi = org.jiopi.framework.CentralConsole.accessModuleConsole(\"").append(rc.moduleName).append("\", \"").append(rc.version).append("\").accessStaticControlPanel(\"").append(rc.registerName).append("\");");
									initMethodContent.append("java.lang.Class toClass = (java.lang.Class)org.jiopi.framework.CentralConsole.objectVariable(cpi, \"wrapped\", null);");
									for(CtField staticField:changeStaticList){
										initMethodContent.append(ctClass.getName()).append(".").append(staticField.getName()).append("=(").append(staticField.getType().getName()).append(")cpi.variable(\"").append(staticField.getName()).append("\", null);");
									}
									//set CONTROL_PANEL_CLASS_FIELD
									initMethodContent.append(ctClass.getName()).append(".").append(CONTROL_PANEL_CLASS_FIELD).append("=toClass;");
									initMethodContent.append("return null;}");
									
									initMethod.setBody(initMethodContent.toString());
								}
							}
							else{
								//set directly 
								ObjectAccessor.Accessible.setVariable(classBeingRedefined, CONTROL_PANEL_CLASS_FIELD, toClass);
								for(CtField staticField:changeStaticList){
									try{
										ObjectAccessor.setVariable(classBeingRedefined,staticField.getName(),ObjectAccessor.variable(toClass, staticField.getName(), null));
									}catch(Exception e){
										logger.warn("redefine "+className+"."+staticField.getName()+" ERROR!",e);
									}
								}
							}
						
						
						//add init static Object
						if(classBeingRedefined == null){
							CtField initField = CtField.make("private final static Object "+INIT_OBJ+"="+INIT_METHOD+"();", ctClass);
							ctClass.addField(initField);
						}
						

						
						bytes = ctClass.toBytecode();
						redefineClasses.put(className, bytes);
						logger.info("redefine context class : "+className +" ok!");
					}catch(Exception e){
						logger.error("redefine "+className+" ERROR! with classBeingRedefined "+classBeingRedefined,e);
					}
				}
				return bytes;
			}
		}
		return null;
	}

}
